using System.Text;
using System.Windows.Forms;

namespace WLWPluginBase.Windows.Forms
{
    /// <summary>
    /// Customized <see cref="RichTextBox"/> control to support auto-indentation and similar features.
    /// </summary>
    public class RichTextBoxEx : RichTextBox
    {
        #region Constants
        private static string TAB = new string(' ', 4);
        #endregion Constants

        #region Fields
        /// <summary>
        /// Flag indicating whether or not trimming will occur when pasting from the clipboard.
        /// </summary>
        private bool trimIndentOnPaste = true;
        #endregion Fields

        #region Properties
        /// <summary>
        /// Overrides the default number of spaces used to replace the TAB character.
        /// </summary>
        public int TabSpaces
        {
            get { return TAB.Length; }
            set { TAB = new string(' ', value); }
        }
        /// <summary>
        /// Flag indicating whether or not trimming will occur when pasting from the clipboard.
        /// </summary>
        public bool TrimIndentOnPaste
        {
            get { return trimIndentOnPaste; }
            set { trimIndentOnPaste = value; }
        }
        /// <summary>
        /// Flag used to determine if Text is available for pasting from the clipboard.
        /// </summary>
        public bool CanPasteTextFromClipboard
        {
            get { return CanPaste(DataFormats.GetFormat(DataFormats.Text)); }
        }
        /// <summary>
        /// Flag indicating whether or not text is selected in the control.
        /// </summary>
        public bool HasSelectedText
        {
            get { return !string.IsNullOrEmpty(SelectedText); }
        }
        /// <summary>
        /// Flag indicating whether or not the control has text.
        /// </summary>
        public bool HasText
        {
            get { return !string.IsNullOrEmpty(Text); }
        }
        #endregion Properties

        #region Overrides
        /// <summary>
        /// Overriden to peek at the keys to control behavior.
        /// </summary>
        protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e)
        {
            // Override the paste logic in the RichTextBox using the keyboard to ensure
            // no RTF formatting is used.
            if ((e.Control && (e.KeyCode == Keys.V)) ||
                (e.Shift && (e.KeyCode == Keys.Insert)))
            {
                PrepareForPaste();
            }

            // Allow base implementation to process.
            base.OnPreviewKeyDown(e);
        }
        /// <summary>
        /// Overriden to process formatting.
        /// </summary>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Tab)
            {
                if (!Indent(e.Shift) && !e.Shift)
                {
                    SelectedText = TAB;
                }
                e.SuppressKeyPress = true;
            }
            else
            {
                base.OnKeyDown(e);
            }
        }
        public new void Paste()
        {
            PrepareForPaste();
            SelectedText = Clipboard.GetText();
        }
        #endregion Overrides

        #region Formatting
        /// <summary>
        /// Process the text currently in the clipboard and pre-format it by replacing unwanted
        /// blank characters.
        /// </summary>
        /// <remarks>
        /// The processing will remove blank characters from the beginning of the lines. It is
        /// useful when pasting copied source code where indentation will be present at the left
        /// most part of the code to be pasted. Saves from having to manually un-indent in the
        /// editor.
        /// </remarks>
        private void PrepareForPaste()
        {
            // Determine if there's text to be processed.
            if (CanPasteTextFromClipboard)
            {
                // Replace all TAB characters with the corresponding number of spaces.
                string text = Clipboard.GetText().Replace("\t", TAB);
                // Remove the content from the clipboard since we are going to re-insert it.
                Clipboard.Clear();
                // Determine if we should remove any unwanted indentation by looking at the line
                // the has the least number of spaces to trim.
                if (TrimIndentOnPaste)
                {
                    // Extract the lines for processing.
                    string[] lines = text.Split('\n');
                    // Make sure we have lines to process.
                    if ((lines != null) && (lines.Length > 0))
                    {
                        // Determine the number of characters that should be trimmed from the beginning.
                        int numCharsToTrim = lines[0].Length - lines[0].TrimStart(new char[] { '\t', ' ' }).Length;
                        // Process all lines.
                        for (int i = 0; i < lines.Length; i++)
                        {
                            // Only process the lines from which we can remove characters.
                            if (lines[i].Length >= numCharsToTrim)
                            {
                                lines[i] = lines[i].Remove(0, numCharsToTrim);
                            }
                        }
                    }
                    // Put the text back together.
                    text = string.Join("\n", lines);
                }
                // Re-add the text to the clipboard for further processing.
                Clipboard.SetText(text);
            }
        }
        /// <summary>
        /// Get the character position representing the start of the given line number.
        /// </summary>
        /// <param name="line">line number for which to determine the character index within the control's text.</param>
        /// <returns>The index within the control's text representing the start of the given line.</returns>
        public int GetLineStartCharIndex(int line)
        {
            int offset = 0;
            for (int i = 0; i < line && i < Lines.Length; i++)
            {
                offset += Lines[i].Length + 1;
            }
            return offset;
        }
        /// <summary>
        /// Get the character position representing the end of the given line number.
        /// </summary>
        /// <param name="line">line number for which to determine the character index within the control's text.</param>
        /// <returns>The index within the control's text representing the end of the given line.</returns>
        public int GetLineEndCharIndex(int line)
        {
            return GetLineStartCharIndex(line) + Lines[line].TrimEnd(new char[] { '\r', '\n' }).Length;
        }
        /// <summary>
        /// Decrement the indentation of the selected text.
        /// </summary>
        public void DecIndent()
        {
            Indent(true);
        }
        /// <summary>
        /// Increment the indentation of the selected text.
        /// </summary>
        public void IncIndent()
        {
            Indent(false);
        }
        /// <summary>
        /// Indents the selected text based on the <see cref="TAB"/> definition, either by adding
        /// or removing the number of spaces represented by <see cref="TAB"/> constant.
        /// </summary>
        /// <param name="decrement">flag indicating whether or not the operation will remove <see cref="TAB"/> characters.</param>
        /// <returns>Flag indicating whehter or not the selected contents were processed.</returns>
        private bool Indent(bool decrement)
        {
            // Assume there was nothing processed.
            bool processed = false;

            // Make sure we have some text to process.
            if (HasText && (decrement || HasSelectedText))
            {
                // Prepare for processing by determining the details of the text to be affected.
                int charStartIndex = SelectionStart;
                int charEndIndex = charStartIndex + SelectionLength;
                int startLine = GetLineFromCharIndex(charStartIndex);
                int endLine = GetLineFromCharIndex(charEndIndex);

                // Process each selected line.
                StringBuilder sb = new StringBuilder();
                for (int i = startLine; i < endLine + 1; i++)
                {
                    string line = Lines[i];
                    if (decrement)
                    {
                        if (line.StartsWith(TAB))
                        {
                            sb.AppendLine(line.Remove(0, TAB.Length));
                        }
                        else
                        {
                            sb.AppendLine(line.TrimStart());
                        }
                    }
                    else
                    {
                        sb.AppendLine(TAB + line);
                    }
                }

                // Re-determine the start and end lines for the text that was processed.
                charStartIndex = GetLineStartCharIndex(startLine);
                charEndIndex = GetLineEndCharIndex(endLine);
                // Highlight the processed text.
                Focus();
                Select(charStartIndex, charEndIndex - charStartIndex);
                SelectedText = sb.ToString().TrimEnd(new char[] {'\r', '\n'});
                if (HasText && (endLine < Lines.Length))
                {
                    charEndIndex = GetLineEndCharIndex(endLine);
                    Select(charStartIndex, charEndIndex - charStartIndex);
                }

                // Indicate some processing took place.
                processed = true;
            }

            // Return the result of the operation.
            return processed;
        }
        #endregion Formatting
    }
}